require "engine.class"
require "mod.class.Actor"
require "engine.interface.PlayerRest"
require "engine.interface.PlayerRun"
require "engine.interface.PlayerHotkeys"
require "engine.interface.PlayerSlide"
require "engine.interface.PlayerMouse"
local Map = require "engine.Map"
local Dialog = require "engine.Dialog"
local ActorTalents = require "engine.interface.ActorTalents"
local DeathDialog = require "mod.dialogs.DeathDialog"
local DamageType = require "engine.DamageType"

--- Defines the player
-- It is a normal actor, with some redefined methods to handle user interaction.<br/>
-- It is also able to run and rest and use hotkeys
module(..., package.seeall, class.inherit(
	mod.class.Actor,
	engine.interface.PlayerRest,
	engine.interface.PlayerRun,
	engine.interface.PlayerHotkeys,
	engine.interface.PlayerMouse,
	engine.interface.PlayerSlide
))

function _M:init(t, no_default)
	t.setup_damtype = true

	t.display=t.display or '@'
	t.color_r=t.color_r or 230
	t.color_g=t.color_g or 230
	t.color_b=t.color_b or 230

	t.player = true
	t.open_door = true
	t.type = t.type or "elemental"
	t.subtype = t.subtype or "player"
	t.faction = t.faction or "players"

	--used to calculate faction for npcs in path of war.
	t.tides_of_war = 0

	t.move_others = true
	t.lite = t.lite or 0
	t.old_life = 0

	mod.class.Actor.init(self, t, no_default)
	engine.interface.PlayerHotkeys.init(self, t)

	self.descriptor = {}
end

	-- only do this once
function _M:SetupDamageTypes()
	if self.setup_damtype == true then
		if self.faction == "air" then
			self.combat.damtype = DamageType.LIGHTNING
			self.resists = { [DamageType.EARTH] = -50, [DamageType.LIGHTNING] = 50 }
		elseif self.faction == "earth" then
			self.combat.damtype = DamageType.EARTH
			self.resists = { [DamageType.EARTH] = 50, [DamageType.LIGHTNING] = -50 }
		elseif self.faction == "fire" then
			self.combat.damtype = DamageType.FIRE
			self.resists = { [DamageType.COLD] = -50, [DamageType.FIRE] = 50 }
		elseif self.faction == "water" then
			self.combat.damtype = DamageType.COLD
			self.resists = { [DamageType.COLD] = 50, [DamageType.FIRE] = -50 }
		elseif self.faction == "mana" then
			self.combat.damtype = DamageType.MANA
			self.resists = { [DamageType.COLD] = 50, [DamageType.EARTH] = 50, [DamageType.FIRE] = 50, [DamageType.LIGHTNING] = 50, [DamageType.MANA] = 50, [DamageType.CHAOS] = -50, }
		elseif self.faction == "chaos" then
			self.combat.damtype = DamageType.CHAOS
			self.resists = { [DamageType.COLD] = -10, [DamageType.EARTH] = -10, [DamageType.FIRE] = -10, [DamageType.LIGHTNING] = -10, [DamageType.MANA] = -50}
		end
		self.setup_damtype = false
	end
	self.combat_bonus = 1
end

function _M:move(x, y, force)
	local moved = mod.class.Actor.move(self, x, y, force)
	if moved then
		game.level.map:moveViewSurround(self.x, self.y, 8, 8)

		local i, nb = 1, 0
		local obj = game.level.map:getObject(self.x, self.y, i)
		while obj do
			nb = nb + 1
			i = i + 1
			obj = game.level.map:getObject(self.x, self.y, i)
		end
		if nb >= 2 then
			game.logSeen(self, "There is more than one object lying here.")
		elseif nb == 1 then
			game.logSeen(self, "There is an item here: %s", game.level.map:getObject(self.x, self.y, 1):getName{do_color=true})
		end
	end
	return moved
end

function _M:act()
	if not mod.class.Actor.act(self) then return end
	
	local o1, item1, inven_id1 = self:findInAllInventories("Orb of Air")
	local o2, item2, inven_id2 = self:findInAllInventories("Orb of Earth")
	local o3, item3, inven_id3 = self:findInAllInventories("Orb of Fire")
	local o4, item4, inven_id4 = self:findInAllInventories("Orb of Water")
	local o5, item4, inven_id4 = self:findInAllInventories("Orb of Mana")
	local o6, item4, inven_id4 = self:findInAllInventories("Orb of Chaos")
		
	-- Set shader HP warning
	if game.fbo_shader and self.life ~= self.old_life then
		if self.life < self.max_life / 2 then game.fbo_shader:setUniform("hp_warning", 1 - (self.life / self.max_life))
		else game.fbo_shader:setUniform("hp_warning", 0) end
	end
	self.old_life = self.life

	-- Clean log flasher
	game.flash:empty()

	-- Resting ? Running ? Otherwise pause
	if not self:restStep() and not self:runStep() and self.player then
		game.paused = true
	elseif not self.player then
		self:useEnergy()
	end

	-- check to see if the portal should be closed :)
	if game.zone.name == "Path of War" and game.level.level == 5 then
		if o1 or o2 or o3 or o4 then
			if game.level.map(25, 25, TERRAIN).name ~= "a dead portal" then
				local q = mod.class.Grid.new{
					show_tooltip=true,
					name="a dead portal",
					desc="The portal appears to be dead again",
					display='*', color=colors.VIOLET, image="terrain/grass.png",
					add_displays = {mod.class.Grid.new{image="terrain/portal_dead.png", dislpay_on_seen=true, display_on_remember=true}},
				}
				q:resolve() q:resolve(nil, true)
				game.zone:addEntity(game.level, q, "terrain", 25, 25)
			end
		end
	end

	-- check to see if completed orb quests
	if self:isQuestStatus("orb-collection", engine.Quest.PENDING) then
		if o1 then self:setQuestStatus("orb-collection", engine.Quest.COMPLETED, "air")
		elseif self:isQuestStatus("orb-collection", engine.Quest.COMPLETED, "air") then self:setQuestStatus("orb-collection", engine.Quest.PENDING, "air") end
		if o2 then self:setQuestStatus("orb-collection", engine.Quest.COMPLETED, "earth")
		elseif self:isQuestStatus("orb-collection", engine.Quest.COMPLETED, "earth") then self:setQuestStatus("orb-collection", engine.Quest.PENDING, "earth") end
		if o3 then self:setQuestStatus("orb-collection", engine.Quest.COMPLETED, "fire")
		elseif self:isQuestStatus("orb-collection", engine.Quest.COMPLETED, "fire") then self:setQuestStatus("orb-collection", engine.Quest.PENDING, "fire") end
		if o4 then self:setQuestStatus("orb-collection", engine.Quest.COMPLETED, "water")
		elseif self:isQuestStatus("orb-collection", engine.Quest.COMPLETED, "water") then self:setQuestStatus("orb-collection", engine.Quest.PENDING, "water") end
	end

	if o1 and self:isQuestStatus("destroy-air", engine.Quest.PENDING, "air-orb") then self:setQuestStatus("destroy-air", engine.Quest.COMPLETED, "air-orb") end
	if o2 and self:isQuestStatus("destroy-earth", engine.Quest.PENDING, "earth-orb") then self:setQuestStatus("destroy-earth", engine.Quest.COMPLETED, "earth-orb") end
	if o3 and self:isQuestStatus("destroy-fire", engine.Quest.PENDING, "fire-orb") then self:setQuestStatus("destroy-fire", engine.Quest.COMPLETED, "fire-orb") end
	if o4 and self:isQuestStatus("destroy-water", engine.Quest.PENDING, "water-orb") then self:setQuestStatus("destroy-water", engine.Quest.COMPLETED, "water-orb") end
	if o5 and self:isQuestStatus("destroy-mana", engine.Quest.PENDING, "mana-orb") then self:setQuestStatus("destroy-mana", engine.Quest.COMPLETED, "mana-orb") end
	if o6 and self:isQuestStatus("destroy-chaos", engine.Quest.PENDING, "chaos-orb") then self:setQuestStatus("destroy-chaos", engine.Quest.COMPLETED, "chaos-orb") end

end

function _M:playerFOV()
	-- Clean FOV before computing it
	game.level.map:cleanFOV()
	-- Compute both the normal and the lite FOV, using cache
	self:computeFOV(self.sight or 20, "block_sight", function(x, y, dx, dy, sqdist)
		game.level.map:apply(x, y, math.max((20 - math.sqrt(sqdist)) / 14, 0.6))
	end, true, false, true)
	self:computeFOV(self.lite, "block_sight", function(x, y, dx, dy, sqdist) game.level.map:applyLite(x, y) end, true, true, true)
end

--- Called before taking a hit, overload mod.class.Actor:onTakeHit() to stop resting and running
function _M:onTakeHit(value, src)
	self:runStop("taken damage")
	self:restStop("taken damage")
	local ret = mod.class.Actor.onTakeHit(self, value, src)
	if self.life < self.max_life * 0.3 then
		local sx, sy = game.level.map:getTileToScreen(self.x, self.y)
		game.flyers:add(sx, sy, 30, (rng.range(0,2)-1) * 0.5, 2, "LOW HEALTH!", {255,0,0}, true)
	end
	return ret
end

function _M:heal(value, src)
	value = 1
	mod.class.Actor.heal(self, value, src)
end

function _M:die(src)
	if self.game_ender then
		engine.interface.ActorLife.die(self, src)
		game.paused = true
		self.energy.value = game.energy_to_act
		game:registerDialog(DeathDialog.new(self))
	else
		mod.class.Actor.die(self, src)
	end
end

function _M:setName(name)
	self.name = name
	game.save_name = name
end

--- Notify the player of available cooldowns
function _M:onTalentCooledDown(tid)
	local t = self:getTalentFromId(tid)

	local x, y = game.level.map:getTileToScreen(self.x, self.y)
	game.flyers:add(x, y, 30, -0.3, -3.5, ("%s available"):format(t.name:capitalize()), {0,255,00})
	game.log("#00ff00#Talent %s is ready to use.", t.name)
end

function _M:levelup()
	mod.class.Actor.levelup(self)

	local x, y = game.level.map:getTileToScreen(self.x, self.y)
	game.flyers:add(x, y, 80, 0.5, -2, "LEVEL UP!", {0,255,255})
	game.log("#00ffff#Welcome to level %d.", self.level)
end

--- Tries to get a target from the user
function _M:getTarget(typ)
	return game:targetGetForPlayer(typ)
end

--- Sets the current target
function _M:setTarget(target)
	return game:targetSetForPlayer(target)
end

local function spotHostiles(self)
	local seen = false
	-- Check for visible monsters, only see LOS actors, so telepathy wont prevent resting
	core.fov.calc_circle(self.x, self.y, 20, function(_, x, y) return game.level.map:opaque(x, y) end, function(_, x, y)
		local actor = game.level.map(x, y, game.level.map.ACTOR)
		if actor and self:reactionToward(actor) < 0 and self:canSee(actor) and game.level.map.seens(x, y) then
			seen = {x=x,y=y,actor=actor}
		end
	end, nil)
	return seen
end

--- Can we continue resting ?
-- We can rest if no hostiles are in sight, and if we need life/mana/stamina (and their regen rates allows them to fully regen)
function _M:restCheck()
	local spotted = spotHostiles(self)
	if spotted then return false, ("hostile spotted (%s%s)"):format(spotted.actor.name, game.level.map:isOnScreen(spotted.x, spotted.y) and "" or " - offscreen") end

	-- Resting improves regen
	local perc = math.min(self.resting.cnt / 10, 4)
	self:heal(0.25 * perc)

	-- Check ressources, make sure they CAN go up, otherwise we will never stop
	if not self.resting.rest_turns then
		if self.life < self.max_life then return true end
	else
		return true
	end

	return false, "all resources and life at maximum"
end

--- Can we continue running?
-- We can run if no hostiles are in sight, and if we no interresting terrains are next to us
function _M:runCheck()
	local spotted = spotHostiles(self)
	if spotted then return false, ("hostile spotted (%s%s)"):format(spotted.actor.name, game.level.map:isOnScreen(spotted.x, spotted.y) and "" or " - offscreen") end

	-- Notice any noticable terrain
	local noticed = false
	self:runScan(function(x, y)
		-- Only notice interresting terrains
		local grid = game.level.map(x, y, Map.TERRAIN)
		if grid and grid.notice then noticed = "interesting terrain" end

		-- Objects are always interresting
		local obj = game.level.map:getObject(x, y, 1)
		if obj then noticed = "object seen" end

	end)
	if noticed then return false, noticed end

	return engine.interface.PlayerRun.runCheck(self)
end

--- Move with the mouse
-- We just feed our spotHostile to the interface mouseMove
function _M:mouseMove(tmx, tmy)
	return engine.interface.PlayerMouse.mouseMove(self, tmx, tmy, spotHostiles)
end

--- Called after running a step
function _M:runMoved()
	self:playerFOV()
end

function _M:doDrop(inven, item)
	self:dropFloor(inven, item, true, true)
	self:sortInven()
	self:useEnergy()

	-- note (25,25) is the dead portal that needs to be activated
	if game.zone.name == "Path of War" and game.level.level == 5 then
		local a = game.level.map:getObject(24, 24, 1)	-- fire
		local b = game.level.map:getObject(26, 24, 1)	-- earth
		local c = game.level.map:getObject(26, 26, 1)	-- water
		local d = game.level.map:getObject(24, 26, 1)	-- air

		if a.name == "Orb of Fire" and b.name == "Orb of Earth" and c.name == "Orb of Water" and d.name == "Orb of Air" then		-- mana
--			game:setAllowedBuild("mana")
			local p = mod.class.Grid.new{
				show_tooltip=true,
				name="Mana Portal",
				desc="The way to the mana temple has been unlocked.",
				display='*', color=colors.VIOLET, image="terrain/grass.png",
				add_displays = {mod.class.Grid.new{image="terrain/portal_alpha1.png", dislpay_on_seen=true, display_on_remember=true}},
				notice = true,
				change_level=1, change_zone="temple-mana"
			}
			p:resolve() p:resolve(nil, true)
			game.zone:addEntity(game.level, p, "terrain", 25, 25)
			game.level.map:particleEmitter(25, 25, 3, "trans-portal", {radius=3})
		elseif a.name == "Orb of Water" and b.name == "Orb of Air" and c.name == "Orb of Fire" and d.name == "Orb of Earth" then	-- chaos
--			game:setAllowedBuild("chaos")
			local q = mod.class.Grid.new{
				show_tooltip=true,
				name="Chaos Portal",
				desc="The way to the chaos temple has been unlocked.",
				display='*', color=colors.VIOLET, image="terrain/grass.png",
				add_displays = {mod.class.Grid.new{image="terrain/portal_alpha1.png", dislpay_on_seen=true, display_on_remember=true}},
				notice = true,
				change_level=1, change_zone="temple-chaos"
			}
			q:resolve() q:resolve(nil, true)
			game.zone:addEntity(game.level, q, "terrain", 25, 25)
			game.level.map:particleEmitter(25, 25, 3, "trans-portal", {radius=3})
		end
	end
	
	self.changed = true
end

function _M:doWear(inven, item, o)
	self:removeObject(inven, item, true)
	local ro = self:wearObject(o, true, true)
	if ro then
		if type(ro) == "table" then self:addObject(inven, ro) end
	elseif not ro then
		self:addObject(inven, o)
	end
	self:sortInven()
	self:useEnergy()
	self.changed = true
end

function _M:doTakeoff(inven, item, o)
	if self:takeoffObject(inven, item) then
		self:addObject(self.INVEN_INVEN, o)
	end
	self:sortInven()
	self:useEnergy()
	self.changed = true
end

function _M:playerPickup()
	-- If 2 or more objects, display a pickup dialog, otehrwise just picks up
	if game.level.map:getObject(self.x, self.y, 2) then
		local d d = self:showPickupFloor("Pickup", nil, function(o, item)
			myobject = item.name
			self:pickupFloor(item, true)
			self:sortInven()
			self.changed = true
			d.title = "Pickup"
			d:used()
		end)
	else
		self:pickupFloor(1, true)
		self:sortInven()
		self:useEnergy()
		self.changed = true
	end
end

function _M:playerDrop()
	local inven = self:getInven(self.INVEN_INVEN)
	self:showInventory("Drop object", inven, nil, function(o, item)
		self:doDrop(inven, item)
	end)
end

function _M:playerWear()
	local inven = self:getInven(self.INVEN_INVEN)
	self:showInventory("Wield/wear object", inven, function(o)
		return o:wornInven() and true or false
	end, function(o, item)
		self:doWear(inven, item, o)
	end)
end

function _M:playerTakeoff()
	self:showEquipment("Take off object", nil, function(o, inven, item)
		self:doTakeoff(inven, item, o)
	end)
end

function _M:playerUseItem(object, item, inven)
	local use_fct = function(o, inven, item)
		self.changed = true
		local ret, no_id = o:use(self)
		if not no_id then
			o:identify(true)
		end
		if ret and ret == "destroy" then
			if o.multicharge and o.multicharge > 1 then
				o.multicharge = o.multicharge - 1
			else
				local _, del = self:removeObject(self:getInven(inven), item)
				if del then
					game.log("You have no more %s.", o:getName{no_count=true, do_color=true})
				else
					game.log("You have %s.", o:getName{do_color=true})
				end
				self:sortInven(self:getInven(inven))
			end
			return true
		end
		self.changed = true
	end

	if object and item then return use_fct(object, inven, item) end

	self:showEquipInven("Use object",
		function(o)
			return o:canUseObject()
		end,
		use_fct,
		true
	)
end

--- Tell us when we are targetted
function _M:on_targeted(act)
-- nothing in here :P
end

------ Quest Events
function _M:on_quest_grant(quest)
	game.logPlayer(self, "#LIGHT_GREEN#Accepted quest '%s'! #WHITE#(Press CTRL+Q to see the quest log)", quest.name)
	self.changed = true
end

function _M:on_quest_status(quest, status, sub)
	if sub then
		game.logPlayer(self, "#LIGHT_GREEN#Quest '%s' status updated! #WHITE#(Press CTRL+Q to see the quest log)", quest.name)
	elseif status == engine.Quest.COMPLETED then
		game.logPlayer(self, "#LIGHT_GREEN#Quest '%s' completed! #WHITE#(Press CTRL+Q to see the quest log)", quest.name)
	elseif status == engine.Quest.DONE then
		game.logPlayer(self, "#LIGHT_GREEN#Quest '%s' is done! #WHITE#(Press CTRL+Q to see the quest log)", quest.name)
	elseif status == engine.Quest.FAILED then
		game.logPlayer(self, "#LIGHT_RED#Quest '%s' is failed! #WHITE#(Press CTRL+Q to see the quest log)", quest.name)
	end
	self.changed = true
end
